#!/usr/bin/python
# -*- coding: utf-8 -*-

#This file is part of pyLot library.
#
# pyLot is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyLot is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with pyLot.  If not, see <http://www.gnu.org/licenses/>.

__author__ = u'Pierre Puiseux, Guillaume Baty'
__copyright__ = u"Copyright 2011-2012 (C) andheo, Université de Pau et des Pays de l'Adour"
__credits__ = [u'Pierre Puiseux', u'Guillaume Baty']
__license__ = "GNU Lesser General Public License"

__date__ = u"7 oct. 2009"
__author__ = u"INRIA pyLot"

__all__ = [u'ShellCommand', u'PythonCommand', u'TextCommand',
           u'ManualCommand', u'SpecialCommand', u'CommonCommand']

import string

from _pylot.minimal.system import ENCODING
from _pylot.minimal.utils import (check_var_naming_convention, is_iterable,
                                check_type, arrange_alias)

from _pylot.minimal.default.impl_commonrun import CommonRun

class CommonCommand (CommonRun):
  u"""
  Command object common part.
  All commands own :
    an identifier (cmd_id)
    an alias
  """

  def __init__ (self, cmd_id, alias=None):
    super(CommonCommand, self).__init__(None)

    check_var_naming_convention(cmd_id)
    check_type(u'alias', alias, unicode, none=True)

    self.cmd_id = cmd_id
    self.alias = arrange_alias(cmd_id, alias)

    self._stdout = None
    self._stderr = None

  def _setstdout (self, stdout):
    if isinstance(stdout, str):
      self._stdout = stdout.decode(ENCODING, u'replace')
    elif isinstance(stdout, unicode) or stdout is None :
      self._stdout = stdout
    else :
      # raise TypeError
      check_type(u'stdout', stdout, [str, unicode], none=True)

  def _setstderr (self, stderr):
    if isinstance(stderr, str):
      self._stderr = stderr.decode(ENCODING, u'replace')
    elif isinstance(stderr, unicode) or stderr is None :
      self._stderr = stderr
    else :
      # raise TypeError
      check_type(u'stderr', stderr, [str, unicode], none=True)

  stdout = property(fget=lambda self: self._stdout,
                    fset=_setstdout)
  stderr = property(fget=lambda self: self._stderr,
                    fset=_setstderr)

  def _kill (self, signal):
    self.thread.kill()

  def _terminate (self):
    self.thread.terminate()

  def _getState (self):
    if self.thread is None :
      return self.NotRunning
    else :
      return self.thread.state

  def _getReturnCode (self):
    if self.thread is None :
      return None
    else :
      return self.thread.return_code

class ManualCommand (CommonCommand):
  u"""
  ManualCommand is a special class designed to be run manually.
  You must change state and return code manually.
  
  if RETURN_CODE isn't defined (is None), RETURN_CODE passes to 0 after run 
  (cannot fail) else RETURN_CODE is kept unchanged.

  This class is used to force special state (fails always for example) or
  in order to handle very simple commands like TextCommand.

  state : self.STATE
  return_code : self.RETURN_CODE
  """
  def __init__ (self, cmd_id=u'ManualCommand', alias=u'Manual Command'):
    super(ManualCommand, self).__init__(cmd_id, alias)

    check_type(u'cmd_id', cmd_id, unicode)
    check_type(u'alias', cmd_id, unicode, none=True)

    self.thread = self
    self.STATE = self.NotRunning
    self.RETURN_CODE = None

  def __unicode__ (self):
    return u'*** ManualCommand %s (rcode=%d)' % (self.cmd_id, self.RETURN_CODE)

  def __str__ (self):
    return unicode(self).encode(ENCODING)

  def _getState (self):
    return self.STATE

  def _getReturnCode (self):
    return self.RETURN_CODE

  def failed (self, return_code=1):
    self.RETURN_CODE = return_code
    return self

  def success (self):
    self.RETURN_CODE = 0
    return self

  def kill (self, signal=9):
    self.RETURN_CODE = 1

class TextCommand (ManualCommand):
  u"""
  The TextCommand is used to display text
  """
  def __init__ (self, text, style):
    u"""
    :param text: the text you want to display
    :type text: str
    :param style: text style
    :type style: str
    """
    check_type(u'text', text, unicode)
    check_type(u'style', style, unicode)

    super(TextCommand, self).__init__(u'text', text)

    self.text = text
    self.style = style

class SpecialCommand (ManualCommand):

  u"""
  The SpecialCommand class is used to add informations to a command group.

  Use static method to get special commands :

  group separator : SpecialCommand.SeparateGroup()
  group start : SpecialCommand.StartGroup()
  group end : SpecialCommand.EndGroup()
  command pipe : SpecialCommand.Pipe()

  .. warning :: 
    This class is reserved for developers
  """

  START_GROUP = u'StartGroup'
  END_GROUP = u'EndGroup'
  SEP_GROUP = u'SeparateGroupe'
  PIPE = u'Pipe'

  def __init__ (self, action):
    u"""
    :param action: specifies action to do. Available actions :
    SpecialCommand.START_GROUP, ...
    """
    super(SpecialCommand, self).__init__(u'specialCommand', action)
    self.action = action

  @staticmethod
  def SeparateGroup ():
    return SpecialCommand(SpecialCommand.SEP_GROUP)

  @staticmethod
  def StartGroup ():
    return SpecialCommand(SpecialCommand.START_GROUP)

  @staticmethod
  def EndGroup ():
    return SpecialCommand(SpecialCommand.END_GROUP)

  @staticmethod
  def Pipe ():
    return SpecialCommand(SpecialCommand.PIPE)

class ShellCommand (CommonCommand):
  u"""
  Handles unique SHELL command and associated process.
  A shell file (a script) must be added as a command.
  """
  # Merge old Command and ProcessInterface classes
  # (old module pylot.core.processhandler)

  def __init__ (self, cmd, args=[], cmd_id=None, alias=None, env_vars=[]):
    check_type(u'cmd', cmd, unicode)
    assert(is_iterable(args))
    args = list(args)
    check_type(u'cmd_id', cmd_id, unicode, none=True)
    check_type(u'alias', alias, unicode, none=True)
    assert(is_iterable(env_vars))
    env_vars = list(args)

    if cmd_id is None :
      cmd_parts = cmd.split()
      cmd_id = u'cmd_%s' % cmd_parts[0]
      for c in string.punctuation :
        cmd_id = cmd_id.replace(c, u'_')
      # remove non ascii characters
      cmd_id = cmd_id.encode(u'ASCII', u'ignore').decode(u'ASCII')
    else :
      cmd_id = unicode(cmd_id)

    self.cmd = cmd
    if alias is None :
      alias = cmd_id

    super(ShellCommand, self).__init__(cmd_id, alias)

    self.args = []
    for i, arg in enumerate(args) :
      if isinstance(arg, str) :
        check_type(u'arg(%d)' % i, arg, unicode)
      elif isinstance(arg, unicode) :
        self.args.append(arg)
      else :
        self.args.append(unicode(arg))

    self.env_vars = env_vars

  def shellUString (self):
#    args = []
#    if self.nice != 0 :
#      args += [u'nice', u'-n', unicode(self.nice)]
#    args.append(unicode(self))
#    return u' '.join(args)
    return unicode(self)

  def __unicode__ (self):
    u"""
    >>> ci = ShellCommand(u'ls', [u'-r', u'/home'])
    >>> unicode(ci)
    u'ls -r /home'
    """
    return u'%s %s' % (self.cmd, u' '.join([unicode(arg) for arg in self.args]))

  def __str__ (self):
    return unicode(self).encode(ENCODING)

class PythonCommand (CommonCommand):
  u"""
  .. warning :: 
  
    ONLY add very simple python functions : 
    - one or two instructions
    - pure python
    - no C++ bindings
    - no threaded instructions

  These restrictions are due to potential thread issues in implementations.
  For example, QThread cannot handle complexe python instructions.
  """

  def __init__ (self, python_method, args=[], kargs={}, cmd_id=u'cmd',
                alias=None):
    super(PythonCommand, self).__init__(cmd_id, alias)

    self.python_method = python_method

    assert (is_iterable(args))
    self.args = args
    self.kargs = kargs

  def __str__ (self):
    u"""
    >>> def print_args (a, b) : print a, b
    >>> pci = PythonCommand(print_args, args=[1], kargs={u'b':2})
    >>> print pci
    print_args (1, b=2)
    """
    args = u', '.join([unicode(arg) for arg in self.args])
    kargs = u', '.join([u'%s=%s' % item for item in self.kargs.items()])
    all_args = []
    if args : all_args.append(args)
    if kargs : all_args.append(kargs)
    return u'%s (%s)' % (self.python_method.__name__, u', '.join(all_args))

  def pyObjects (self):
    u"""
    returns (python_method, args, kargs)
    """
    return self.python_method, self.args, self.kargs

def command_type (command):
  u"""
  Returns command type as string.
  possible outputs : shell, python, manual, text, None (unknown)
  
  >>> command_type(ShellCommand(u'a', [u'b']))
  u'shell'

  """
  if issubclass(type(command), ManualCommand) :
    return u'manual'
  elif issubclass(type(command), TextCommand) :
    return u'text'
  elif issubclass(type(command), ShellCommand) :
    return u'shell'
  elif issubclass(type(command), PythonCommand) :
    return u'python'
  else :
    return False

def is_command (cmd_type, command):
  u"""
  >>> is_command(u'shell', ShellCommand(u'a', [u'b']))
  True
  >>> is_command(u'shell', ManualCommand(u'a'))
  False
  """
  assert(type(cmd_type) is unicode)
  return command_type(command) == cmd_type.lower()

if __name__ == u'__main__':
  import doctest
  doctest.testmod()

